/*
 * cve-check-tool.c
 *
 * Copyright (C) 2015-2016 Sergey Popovich <popovich_sergei@mail.ua>.
 *
 * cve-check-tool is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 */

#define _GNU_SOURCE

#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include "core.h"
#include "cve-db-lock.h"
#include "cve-string.h"
#include "util.h"

static const short int locktype2l_type[LT_MAX + 1] = {
            [LT_READ] = F_RDLCK, [LT_WRITE] = F_WRLCK,
};

static const char locktype2string[LT_MAX + 1][sizeof("write")] = {
            [LT_READ] = "read", [LT_WRITE] = "write",
};

static int db_lock_fd = -1;
static cve_string *db_lock_fname;

#ifndef O_NOFOLLOW
#define O_NOFOLLOW 0
#endif

bool cve_db_lock_init(const char *db_path)
{
        const int flags = O_RDWR | O_CREAT | O_NONBLOCK | O_NOFOLLOW;
        const mode_t mode = S_IRUSR | S_IWUSR;

        assert(db_lock_fd < 0);
        assert(db_lock_fname == NULL);
        assert(db_path != NULL);

        db_lock_fname = make_db_dot_fname(db_path, "cve.lock");
        if (!db_lock_fname) {
                return false;
        }

        db_lock_fd = open(db_lock_fname->str, flags, mode);
        if (db_lock_fd < 0) {
                cve_string_free(db_lock_fname);
                db_lock_fname = NULL;
                return false;
        }

        return true;
}

void cve_db_lock_fini(void)
{
        assert(db_lock_fd >= 0);
        assert(db_lock_fname != NULL);

        close(db_lock_fd);
        db_lock_fd = -1;

        unlink(db_lock_fname->str);
        cve_string_free(db_lock_fname);
        db_lock_fname = NULL;
}

bool cve_db_lock(locktype lt, int wait)
{
        const char *lt_str;
        unsigned int waited;

        assert(lt < LT_MAX + 1);
        assert(db_lock_fd >= 0);

        lt_str = locktype2string[lt];

        if (wait < 0) {
                waited = wait = 2;
        } else {
                waited = 0;
        }

        do {
                struct flock fl = {
                        .l_type = locktype2l_type[lt], .l_whence = SEEK_SET,
                };
                int ret;

                ret = fcntl(db_lock_fd, F_SETLK, &fl);
                if (!ret) {
                        return true;
                }
                if (errno != EAGAIN && errno != EACCES) {
                        fprintf(stderr, "Error acquiring database lock: %s\n", strerror(errno));
                        break;
                }

                if (waited % 2) {
                        goto sleep;
                }
                fputs("Another app holds the lock on database", stderr);
                if (wait) {
                        int remaining = wait - waited;
                        if (remaining <= 0) {
                                fprintf(stderr, "; %s lock is not acquired\n", lt_str);
                                break;
                        }
                        fprintf(stderr, "; acquiring %s lock within %ds ...", lt_str, remaining);
                } else {
                        fputs("; waiting indefinitely", stderr);
                }
                fputc('\n', stderr);
        sleep:
                sleep(1);
                waited++;
                if (wait && waited >= (unsigned int)wait) {
                        /* last round: make it even */
                        waited = (wait + 1) & ~1;
                }
        } while (1);

        return false;
}

void cve_db_unlock(void)
{
        struct flock fl = {
                .l_type = F_UNLCK, .l_whence = SEEK_SET,
        };
        int ret;

        ret = fcntl(db_lock_fd, F_SETLK, &fl);

        assert(ret == 0);
}

/*
 * Editor modelines  -  https://www.wireshark.org/tools/modelines.html
 *
 * Local variables:
 * c-basic-offset: 8
 * tab-width: 8
 * indent-tabs-mode: nil
 * End:
 *
 * vi: set shiftwidth=8 tabstop=8 expandtab:
 * :indentSize=8:tabSize=8:noTabs=true:
 */
